import { Event } from "vscode";
import { traceInfo } from "../../../../logging";
import { reportInterpretersChanged } from "../../../../proposedApi";
import { arePathsSame, pathExists } from "../../../common/externalDependencies";
import { EQEnvInfo } from "../../info";
import { areSameEnv, getEnvPath } from "../../info/env";
import { BasicEQEnvCollectionChangedEvent, EQEnvCollectionChangedEvent, EQEnvsWatcher } from "../../watcher";

export interface IEnvsCollectionCache {

    onChanged: Event<BasicEQEnvCollectionChangedEvent>;

    clearCache(): Promise<void>;

    updateEnv(oldValue: EQEnvInfo, newValue: EQEnvInfo | undefined): void;

    addEnv(env: EQEnvInfo, hasCompleteInfo?: boolean): void;

    flush(): Promise<void>;

    validateCache(): Promise<void>;

    getAllEnvs(): EQEnvInfo[];
}

interface IPersistentStorage {
    load(): Promise<EQEnvInfo[]>;
    store(envs: EQEnvInfo[]): Promise<void>;
}
export type EQEnvCompleteInfo = { hasCompleteInfo?: boolean } & EQEnvInfo;
export class EQEnvInfoCache extends EQEnvsWatcher<EQEnvCollectionChangedEvent>
    implements IEnvsCollectionCache {
    private envs: EQEnvCompleteInfo[] = [];

    constructor(private readonly persistentStorage: IPersistentStorage) {
        super();
    }

    public async validateCache(): Promise<void> {
        /**
         * We do check if an env has updated as we already run discovery in background
         * which means env cache will have up-to-date envs eventually. This also means
         * we avoid the cost of running lstat. So simply remove envs which no longer
         * exist.
         */
        const areEnvsValid = await Promise.all(this.envs.map((e) => pathExists(e.executable.filename)));
        const invalidIndexes = areEnvsValid
            .map((isValid, index) => (isValid ? -1 : index))
            .filter((i) => i !== -1)
            .reverse(); // Reversed so indexes do not change when deleting
        invalidIndexes.forEach((index) => {
            const env = this.envs.splice(index, 1)[0];
            this.fire({ old: env, new: undefined });
            reportInterpretersChanged([
                { path: getEnvPath(env.executable.filename, env.location).path, type: 'remove' },
            ]);
        });
    }

    public getAllEnvs(): EQEnvInfo[] {
        return this.envs;
    }

    public addEnv(env: EQEnvCompleteInfo, hasCompleteInfo?: boolean): void {
        const found = this.envs.find((e) => areSameEnv(e, env));
        if (hasCompleteInfo) {
            env.hasCompleteInfo = true;
        }
        if (!found) {
            this.envs.push(env);
            this.fire({ new: env });
            reportInterpretersChanged([{ path: getEnvPath(env.executable.filename, env.location).path, type: 'add' }]);
        }
    }

    public updateEnv(oldValue: EQEnvInfo, newValue: EQEnvInfo | undefined): void {
        const index = this.envs.findIndex((e) => areSameEnv(e, oldValue));
        if (index !== -1) {
            if (newValue === undefined) {
                this.envs.splice(index, 1);
            } else {
                this.envs[index] = newValue;
            }
            this.fire({ old: oldValue, new: newValue });
            reportInterpretersChanged([
                {
                    path: getEnvPath(oldValue.executable.filename, oldValue.location).path,
                    type: newValue ? 'update' : 'remove',
                },
            ]);
        }
    }

    public getCompleteInfo(path: string): EQEnvInfo | undefined {
        // `path` can either be path to environment or executable path
        let env = this.envs.find((e) => arePathsSame(e.location, path));
        if (env?.hasCompleteInfo) {
            return env;
        }
        env = this.envs.find((e) => areSameEnv(e, path));
        return env?.hasCompleteInfo ? env : undefined;
    }

    public async clearAndReloadFromStorage(): Promise<void> {
        this.envs = await this.persistentStorage.load();
    }

    public async flush(): Promise<void> {
        if (this.envs.length) {
            traceInfo('Environments added to cache', JSON.stringify(this.envs));
            this.envs.forEach((e) => {
                e.hasCompleteInfo = true;
            });
            await this.persistentStorage.store(this.envs);
        }
    }

    public clearCache(): Promise<void> {
        this.envs.forEach((e) => {
            this.fire({ old: e, new: undefined });
        });
        reportInterpretersChanged([{ path: undefined, type: 'clear-all' }]);
        this.envs = [];
        return Promise.resolve();
    }
}
export async function createCollectionCache(storage: IPersistentStorage): Promise<EQEnvInfoCache> {
    const cache = new EQEnvInfoCache(storage);
    await cache.clearAndReloadFromStorage();
    await cache.validateCache();
    return cache;
}